
package com.x52im.rbchat.logic.chat_root.impl;

import android.util.Log;

import com.x52im.rbchat.IMClientManager;
import com.x52im.rbchat.MyApplication;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;
import com.x52im.rainbowchat.im.dto.ChatType;
import com.x52im.rainbowchat.im.dto.MsgType;
import com.x52im.rainbowchat.im.dto.RevokedMeta;
import com.x52im.rbchat.cache.MessagesProvider;
import com.x52im.rbchat.logic.chat_root.model.Message;
import com.x52im.rbchat.sqlite.ChatHistoryTable;
import com.x52im.rbchat.sqlite.GroupChatHistoryTable;
import com.x52im.rbchat.utils.BroadcastToolKits;

import net.x52im.mobileimsdk.android.utils.MBThreadPoolExecutor;

import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;

import androidx.annotation.Nullable;

/**
 * 消息"撤回"全局管理器。
 * <p>
 * 【该管理器的作用】：
 * 由于实时消息撤回指令跟其它实时指令一样，都是异步发出和异步应答的（消息撤回指令等待应答的目的是
 * 确保消息撤回指令已送达，否则将影响撤回功能的用户体验，这很重要），所以本类中使用一个Map管理当前
 * 正在被撤回中的消息（即key=撤回指令的fp指纹码，value=当前正在被撤回消息在消息列表中的Message
 * 数据模型对象），当收到撤回指令的的ACK应答包时，就表示撤回指令已送达，UI上就可以取消跟微信\QQ
 * 一样的菊花进度提示框架、同时进行本地消息撤回的余下逻辑（sqlite更新、ui显示更新等）。
 * <p>
 * 【一个疑问】：
 * 既然消息撤回指令使用实时指令，因为异步应答让事情应该的稍难处理，那干嘛不像其它需要即时得到反馈的
 * 指令那样通过http接口发出呢？原因是消息撤回涉及到陌生人聊天、好友聊天、群聊天，http接口到服务端后
 * ，服务端那头再进行撤回的下行逻辑（即通知被撤回方、以及一些离线处理逻辑等等），需要区分3种聊天模式
 * 的话，就会多很多额外的代码，会把事情搞的更复杂。所以目前这样，利用聊天通道，以实时指令方式送出，作
 * 为一种特殊的"聊天"消息，就可以借用现有的完整消息发送、应答、离线逻辑、消息记录处理逻辑，就能少掉很
 * 多额外代码，代码实现上也更优雅。
 * <p>
 * 【跟UI层的解耦合】：
 * 由于撤回功能，需要像微信、qq那样在撤回成功应答前需要显示一个菊花进度提示框，当收到应答后需要取消进
 * 度的显示，而本管理器作为数据模型和核心逻辑层面的实现，不应跟UI产生耦合，所以目前的实现思路就是：当
 * 撤回动作开始时就由聊天界面来显示一个进度提示框架，到后台收到撤回应时，再通过系统广播（当然也可以
 * EventBus这种框架）通知聊天界面取消进度提示的显示，这样就能跟UI进行优雅解偶了！
 *
 * @author JackJiang
 * @since 7.3
 */
public class MessageRevokingManager {

    private static String TAG = MessageRevokingManager.class.getSimpleName();

    /** 正在被"撤回"的消息集合（虽然实际实用时，一般集合中只有一条，但技术实现上能支持多条，目的对用户的非正常操作进行最大限度的容错） */
    private final ConcurrentHashMap<String, MessageBeRevoke> beRevokingMessages = new ConcurrentHashMap<>();

    public MessageRevokingManager(){
    }

    /**
     * 开始撤回。
     *
     * @param fpForRevokeCmd 发出的消息"撤回"指令对应的指纹码
     * @param messageBeRevoke 要撤回的消息位于聊天列表数据模型中的消息对象
     */
    public void revokeStart(String fpForRevokeCmd, MessageBeRevoke messageBeRevoke){
        Log.i(TAG, "【消息撤回】[revokeStart]fpForRevokeCmd="+fpForRevokeCmd+", messageBeRevoke="+messageBeRevoke);
        if(fpForRevokeCmd != null && messageBeRevoke != null) {
//            if (!messages.containsKey(fpForRevokeCmd))
                beRevokingMessages.put(fpForRevokeCmd, messageBeRevoke);
        }
        else{
            Log.w(TAG, "【消息撤回】无效的参数，revokeStart无法继续，fpForRevokeCmd="+fpForRevokeCmd+"、messageBeRevoke="+messageBeRevoke);
        }
    }

    /**
     * 已收到撤回指令应答（可以认为将被或已被对方收到）。
     *
     * @param fpForRevokeCmd 发出的消息"撤回"指令对应的指纹码
     * @return true表示集合中存在该fp
     */
    public boolean revokeCmdBeRecieved(String fpForRevokeCmd){
        MessageBeRevoke beRevoking = this.beRevokingMessages.get(fpForRevokeCmd);
        if(beRevoking != null){
            Log.i(TAG, "【消息撤回】[revokeCmdBeRecieved]收到fpForRevokeCmd="+fpForRevokeCmd+"的应答，且查【有】此fp，将继续往下执行消息撤回功能的余下逻辑.....");

            try{
                fireRevokeSucess(fpForRevokeCmd, beRevoking);
            }
            catch (Exception e){
                Log.w(TAG, e);
            }
            finally {
                this.beRevokingMessages.remove(fpForRevokeCmd);
            }

            return true;
        }
        else{
            Log.i(TAG, "【消息撤回】[revokeCmdBeRecieved]收到fpForRevokeCmd="+fpForRevokeCmd+"的应答，且查【无!】此fp，此条应答将被忽额。");
        }

        return false;
    }

    /**
     * 消息"撤回"指令没有成功送出（可能是本地网络有问题）。
     *
     * @param fpForRevokeCmd 发出的消息"撤回"指令对应的指纹码
     * @return true表示集合中存在该fp
     */
    public boolean revokeCmdBeLost(String fpForRevokeCmd){
        MessageBeRevoke beRevoking = this.beRevokingMessages.get(fpForRevokeCmd);
        if(beRevoking != null){
            Log.i(TAG, "【消息撤回】【消息撤回】[revokeCmdBeLost]fpForRevokeCmd="+fpForRevokeCmd+"无法送达，且查【有】此fp，将从本管理器中删除此条\"撤回中\"消息的对象应用哦。");
            this.beRevokingMessages.remove(fpForRevokeCmd);

            return true;
        }
        else{
            Log.i(TAG, "【消息撤回】[revokeCmdBeLost]fpForRevokeCmd="+fpForRevokeCmd+"无法送达，且查【无!】此fp，什么也不用做。");
        }

        return false;
    }

    /**
     * 消息撤回成功后要做的事。
     *
     * @param fpForRevokeCmd 发出的消息"撤回"指令对应的指纹码
     * @param messageBeRevoke 要撤回的消息位于聊天列表数据模型中的消息对象
     * @throws Exception
     */
    private void fireRevokeSucess(final String fpForRevokeCmd, MessageBeRevoke messageBeRevoke) throws Exception{

        Log.i(TAG, "【消息撤回】消息撤回成功，马上开始执行真正的撤回逻辑 ==> messageBeRevoke = "+messageBeRevoke);

        if(messageBeRevoke == null) {
            Log.w(TAG, "【消息撤回】messageBeRevoke == null！");
            return;
        }

        // 开始直正的撤回逻辑（多线程后台执行，防止耗时）
        MBThreadPoolExecutor.runInBackground(() -> {
            // 被撤回消息来自于哪种聊天模式
            final int chatType = messageBeRevoke.getChatType();
            // 被撤回消息所处聊天列表中的数据模型对象引用
            final Message message = messageBeRevoke.getMessage();

            boolean isGroupChat = (chatType == ChatType.CHAT_TYPE_GROUP$CHAT);
            // 被撤回消息的指纹码（如果是群聊消息，则这是被撤回消息的父指纹码）
            String fpForMessage = (isGroupChat ? message.getFingerPrintOfParent() : message.getFingerPrintOfProtocal());
            // 构造被撤回消息内容对象（后续发出的指令内容等，就是这个对象）
            RevokedMeta content = constructRevokedMetaForOperator(fpForMessage
                    // 是群聊 且 撤回的是别人的消息时，需要传入被撤回消息发送者的uid
                    , isGroupChat && !message.isOutgoing()?message.getSenderId():null
                    // 是群聊 且 撤回的是别人的消息时，需要传入被撤回消息发送者的昵称
                    , isGroupChat && !message.isOutgoing() ? message.getSenderDisplayName():null);

            //*** 更新本地sqlite数据库
            updateSQLiteForMessage(chatType, fpForMessage, content);

            //*** 主线程中进行ui更新
            MBThreadPoolExecutor.runOnMainThread(() -> {
                //*** 更新消息列表数据对象内容
                boolean sucess = updateModelForMessage(content, message, fpForRevokeCmd, fpForMessage);
                if (sucess) {
                    Log.i(TAG, "【消息撤回】主动撤回消息时，updateModelForMessage成功了。(content="+content+"，message="+message+"，fpForRevokeCmd="+fpForRevokeCmd+")");
                }
                else{
                    Log.w(TAG, "【消息撤回】主动撤回消息时，updateModelForMessage失败了！(content="+content+"，message="+message+"，fpForRevokeCmd="+fpForRevokeCmd+")");
                }

                //*** 更新消息列表中"引用"了被撤回消息的这些消息上的引用状态字段值
                MessageRevokingManager.updateModelForQuoteMessages(chatType, messageBeRevoke.getToId(), fpForMessage);

                //*** 通知UI层刷新显示
                BroadcastToolKits.revokeCMDRecieved_SEND(MyApplication.getInstance2(), fpForRevokeCmd, fpForMessage);
            });
        });
    }

    public void clear(){
        if(this.beRevokingMessages != null)
            this.beRevokingMessages.clear();
    }

    /**
     * 更新本地数据库。
     *
     * @param chatType 聊天模式
     * @param fpForMessage 被撤回消息的指纹码（如果是群聊消息，则此值应取它的fingerPringOfParent值哦）
     * @param textObj 被撤回消息的新内容对象
     * @return 影响行数，-1表示出错了，0表示没有更新，否则表示更新影响的行数
     */
    public static long updateSQLiteForMessage(int chatType, String fpForMessage, RevokedMeta textObj){

        RosterElementEntity localRee = MyApplication.getInstance2().getIMClientManager().getLocalUserInfo();
        if(localRee == null) {
            Log.w(TAG, "【消息撤回】localRee == null，updateSQLiteForMessage不能继续！");
            return -1;
        }

        if(textObj == null) {
            Log.w(TAG, "【消息撤回】textObj == null，updateSQLiteForMessage不能继续！");
            return -1;
        }

        if(chatType == ChatType.CHAT_TYPE_FREIDN$CHAT || chatType == ChatType.CHAT_TYPE_GUEST$CHAT ){
            ChatHistoryTable chatMessageTable = null;
            try {
                // 初始化表操作对象
                chatMessageTable = ChatHistoryTable.getInstance(MyApplication.getInstance2());
                chatMessageTable.open();

                // 保存消息到本地数据库
                return chatMessageTable.updateForRevoke(localRee.getUser_uid(), fpForMessage, textObj);
            }
            catch (Exception e) {
                Log.w(TAG, e);
                return -1;
            }
            finally {
                try{
                    if(chatMessageTable != null)
                        chatMessageTable.close();
                }
                catch (Exception e){
                }
            }
        }
        else if(chatType == ChatType.CHAT_TYPE_GROUP$CHAT){
            GroupChatHistoryTable chatMessageTable = null;
            try {
                // 初始化表操作对象
                chatMessageTable = GroupChatHistoryTable.getInstance(MyApplication.getInstance2());
                chatMessageTable.open();

                // 保存消息到本地数据库
                return chatMessageTable.updateForRevoke(localRee.getUser_uid(), fpForMessage, textObj);
            }
            catch (Exception e) {
                Log.w(TAG, e);
                return -1;
            }
            finally {
                try{
                    if(chatMessageTable != null)
                        chatMessageTable.close();
                }
                catch (Exception e){
                }
            }
        }
        else{
            Log.w(TAG, "【消息撤回】未知的chatType="+chatType+"，updateSQLiteForMessage无法继续！");
            return -1;
        }
    }

    /**
     * 更新消息列表数据对象内容。
     *
     * @param content 更新内容
     * @param message 要撤回的消息位于聊天列表数据模型中的消息对象
     * @return true表示更新成功，否则不成功
     */
    public static boolean updateModelForMessage(RevokedMeta content, Message message, String fpForRevokeCmd, String fpForMessage){
        if (content != null) {
            // 当是文本消息时就不清理消息内容了（以便稍后实现消息撤回后的"重新编辑"功能）
            if (message.getMsgType() == MsgType.TYPE_TEXT) {
                content.setOriginalContent(message.getText());
            }
            message.setText(RevokedMeta.toJSON(content));
            message.setMsgType(MsgType.TYPE_REVOKE);

//            //*** 通知UI层刷新显示
//            BroadcastToolKits.revokeCMDRecieved_SEND(MyApplication.getInstance2(), fpForRevokeCmd, fpForMessage);

            return true;
        }
        return false;
    }

    /**
     * 更新消息列表中"引用"了被撤回消息的这些消息上的引用状态字段值。
     *
     * @param fromId 聊天对象id（单聊uid或群id）
     * @param fpForOriginalMessage 被引用消息的指纹码
     * @return true表示更新成功，否则不成功
     */
    public static boolean updateModelForQuoteMessages(int chatType, String fromId, String fpForOriginalMessage) {
        try {
            IMClientManager imc = MyApplication.getInstance2().getIMClientManager();

            MessagesProvider messagesProvider = null;
            if (chatType == ChatType.CHAT_TYPE_FREIDN$CHAT || chatType == ChatType.CHAT_TYPE_GUEST$CHAT) {
                messagesProvider = imc.getMessagesProvider();
            } else if (chatType == ChatType.CHAT_TYPE_GROUP$CHAT) {
                messagesProvider = imc.getGroupsMessagesProvider();
            }

            if(messagesProvider == null) {
                Log.w(TAG, "【消息撤回-更新引用消息状态】未知的chatType=" + chatType + "，它对应的messagesProvider==null, updateModelForQuoteMessages 无法继续！");
                return false;
            }

            // 查找所有引用了原消息的消息对象
            ArrayList<Message> allQuoteMessages = messagesProvider.findMessagesByQuoteFingerPrint(fromId, fpForOriginalMessage);
            if (allQuoteMessages.size() > 0) {
                for (Message m : allQuoteMessages) {
                    if (m != null) {
                        // 更新引用状态为1（表示原消息已被撤回）
                        m.setQuote_status(1);
                    }
                }

                Log.i(TAG, "【消息撤回-更新引用消息状态】被撤回消息时，updateModelForQuoteMessage成功了（影响消息条数=" + allQuoteMessages.size() + "）。");
            } else {
                Log.w(TAG, "【消息撤回-更新引用消息状态】被撤回消息时，updateModelForQuoteMessage完成了，但引用消息数为空，没有找到引用它的消息哦。");
            }

            return true;
        }
        catch (Exception e) {
            Log.w(TAG, e);
        }

        return false;
    }

    /**
     * 为消息"撤回"发起者构建撤回指令内容对象。
     *
     * @param fpForMessage 被撤回消息的指纹码（如果是群聊消息，则此值应取它的fingerPringOfParent值哦）
     * @param beUid 被"撤回"者的uid（当前用于群聊时，由管理员撤回其它群员消息时存入被撤回消息的发送者uid，其它余情况下本参数为空！）
     * @param beNickName 被"撤回"者的昵称（当前用于群聊时，由管理员撤回其它群员消息时存入被撤回消息的发送者uid，其它余情况下本参数为空！）
     * @return 新对象
     */
    public static RevokedMeta constructRevokedMetaForOperator(String fpForMessage, @Nullable String beUid, @Nullable String beNickName){
        RosterElementEntity localRee = MyApplication.getInstance2().getIMClientManager().getLocalUserInfo();
        if(localRee == null)
            return null;

        RevokedMeta rm = new RevokedMeta(localRee.getUser_uid(), localRee.getNickname(), fpForMessage);
        rm.setBeUid(beUid);
        rm.setBeNickName(beNickName);
        return rm;
    }

    /**
     * 处理收到的消息"撤回"指令逻辑。
     *
     * @param chatType       聊天类型，see {@link ChatType}
     * @param fpForRevokeCMD 撤回指令本身的指纹码
     * @param fromId         一对一聊天时此参数表示对方的uid，群聊时表示是群id
     * @param messageContent 消息撤回指令的内容
     */
    public static void processRevokeMessage_incoming(int chatType, String fpForRevokeCMD, String fromId, String messageContent) {
        IMClientManager imc = MyApplication.getInstance2().getIMClientManager();

        RevokedMeta messageContentObj = RevokedMeta.fromJSON(messageContent);
        if (messageContentObj != null && messageContentObj.getFpForMessage() != null) {

            //*** 更新本地sqlite数据库
            long row = MessageRevokingManager.updateSQLiteForMessage(chatType, messageContentObj.getFpForMessage(), messageContentObj);
            Log.i(TAG, "【消息撤回】被撤回消息时，updateSQLiteForMessage完成，影响row=" + row + "。(messageContentObj=" + messageContentObj + "，fpForRevokeCmd=" + fpForRevokeCMD + ")");

            //*** 更新消息列表数据对象内容本身的撤回信息等
            Message originalMessage = null;
            if (chatType == ChatType.CHAT_TYPE_FREIDN$CHAT || chatType == ChatType.CHAT_TYPE_GUEST$CHAT) {
                originalMessage = imc.getMessagesProvider().findMessageByFingerPrint(fromId, messageContentObj.getFpForMessage());
            } else if (chatType == ChatType.CHAT_TYPE_GROUP$CHAT) {
                originalMessage = imc.getGroupsMessagesProvider().findMessageByParentFingerPrint(fromId, messageContentObj.getFpForMessage());
            } else {
                Log.w(TAG, "【消息撤回】未知的chatType=" + chatType + ", processRevokeMessage_incoming无法继续！");
                return;
            }
            if (originalMessage != null) {
                boolean sucess = MessageRevokingManager.updateModelForMessage(messageContentObj, originalMessage, fpForRevokeCMD, messageContentObj.getFpForMessage());
                if (sucess) {
                    Log.i(TAG, "【消息撤回】被撤回消息时，updateModelForMessage成功了。(messageContentObj=" + messageContentObj + "，fpForRevokeCmd=" + fpForRevokeCMD + ")");
                } else {
                    Log.w(TAG, "【消息撤回】被撤回消息时，updateModelForMessage失败了！(messageContentObj=" + messageContentObj + "，originalMessage=" + originalMessage + "，fpForRevokeCmd=" + fpForRevokeCMD + ")");
                }
            } else {
                Log.w(TAG, "【消息撤回】被撤回消息时，正准备updateModelForMessage，但数据为空，originalMessage=null");
            }

            //*** 更新消息列表中"引用"了被撤回消息的这些消息上的引用状态字段值
            MessageRevokingManager.updateModelForQuoteMessages(chatType, fromId, messageContentObj.getFpForMessage());

            //*** 通知UI层刷新显示
            BroadcastToolKits.revokeCMDRecieved_SEND(MyApplication.getInstance2(), fpForRevokeCMD, messageContentObj.getFpForMessage());
        } else {
            Log.w(TAG, "【消息撤回】被撤回消息时，正准备updateSQLiteForMessage等，但数据为空，messageContentObj=" + messageContentObj);
        }
    }

    public static class MessageBeRevoke {
        private int chatType = -1;
        private String toId;
        private Message message;

        private MessageBeRevoke(int chatType,  String toId, Message message) {
            this.chatType = chatType;
            this.toId = toId;
            this.message = message;
        }

        public int getChatType() {
            return chatType;
        }

        public Message getMessage() {
            return message;
        }

        public String getToId() {
            return toId;
        }

        public void setToId(String toId) {
            this.toId = toId;
        }

        @Override
        public String toString(){
            return "[chatType = "+chatType+", toID  = "+toId+", message="+ message.toString()+"]";
        }

        public static MessageBeRevoke create(int chatType, String toId, Message message){
            if(chatType <0 || toId == null || message == null)
                return null;
            return new MessageBeRevoke(chatType, toId, message);
        }
    }
}
